/*
 * Copyright (C) 2012-2013 Matthias Zeimer and the miniframes project group.
 * All rights reserved. 
 * 
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/EPL-1.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.miniframes.minivalues;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.UUID;

/**
 * <p>
 * An utility class for serialisation support for the properties of a
 * {@link Value}.
 * </p>
 * 
 * <p>
 * TODO Doc serialisation string format.
 * </p>
 * 
 * <p>
 * The methods of this class have a functional behaviour by this all are static.
 * This class is not intended to be instantiated.
 * </p>
 * 
 * <p>
 * Unless otherwise noted, passing a null argument to any method may result in a
 * {@link NullPointerException}.
 * </p>
 * 
 * @author Matthias Zeimer
 * @version 1
 * @since 0.1, 18.04.2013
 */
final class SerialisationSupport {

    private static interface SerialisationStrategy {
        void serialise(StringBuilder appendTo, Object value)
                throws ClassCastException;
    }

    private final static String SEPARATOR = ",";

    private final static Map<Class<?>, SerialisationStrategy> STRATEGIES;
    static {
        STRATEGIES = new IdentityHashMap<Class<?>, SerialisationStrategy>(2); // TODO
                                                                              // expected
                                                                              // max
                                                                              // size
                                                                              // anpassen!

        // The 8 Java-primitives (wrapper types used here):
        STRATEGIES.put(Byte.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Byte.class.cast(value));
                appendTo.append("b");
            }
        });
        STRATEGIES.put(Short.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Short.class.cast(value));
                appendTo.append("s");
            }
        });
        STRATEGIES.put(Integer.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Integer.class.cast(value));
                appendTo.append("i");
            }
        });
        STRATEGIES.put(Long.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Long.class.cast(value));
                appendTo.append("L");
            }
        });
        STRATEGIES.put(Float.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Float.class.cast(value));
                appendTo.append("f");
            }
        });
        STRATEGIES.put(Double.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Double.class.cast(value));
                appendTo.append("d");
            }
        });
        STRATEGIES.put(Boolean.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(Boolean.class.cast(value));
            }
        });
        STRATEGIES.put(Character.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append("'");
                appendTo.append(Character.class.cast(value).toString().replace(
                        "\\", "\\\\").replace("'", "\\'"));
                appendTo.append("'");
            }
        });

        // Other supported types:
        STRATEGIES.put(String.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append("\"");
                appendTo.append(String.class.cast(value).toString().replace(
                        "\\", "\\\\").replace("\"", "\\\""));
                appendTo.append("\"");
            }
        });
        STRATEGIES.put(BigInteger.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(BigInteger.class.cast(value).toString());
                appendTo.append("bi");
            }
        });
        STRATEGIES.put(BigDecimal.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(BigDecimal.class.cast(value).toString());
                appendTo.append("bd");
            }
        });
        STRATEGIES.put(MathContext.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(MathContext.class.getName());
                appendTo.append("[");
                appendTo.append(MathContext.class.cast(value).toString());
                appendTo.append("]");
            }
        });
        STRATEGIES.put(UUID.class, new SerialisationStrategy() {
            @Override
            public void serialise(StringBuilder appendTo, final Object value)
                    throws ClassCastException {
                appendTo.append(UUID.class.getName());
                appendTo.append("[");
                appendTo.append(UUID.class.cast(value).toString());
                appendTo.append("]");
            }
        });
        

        // TODO Add UnmodifiableDate

        // STRATEGIES.put(BigDecimal.class, new SerialisationStrategy() {
        // @Override
        // public void serialise(StringBuilder appendTo, final Object value)
        // throws ClassCastException {
        // return BigDecimal.class.cast(value).toString();
        // }
        // });
    }

    /**
     * <p>
     * Serialise the given array of properties to a String in above described
     * format.
     * </p>
     * 
     * @param properties
     *            An array of the properties to be serialised. This array may
     *            only contain types allowed as fields of a {@link Value} class.
     *            The argument may never be {@code null}.
     * @return The serialised version, never {@code null}, never
     *         {@linkplain String#isEmpty() empty}.
     * @throws NullPointerException
     *             if {@code properties} is {@code null}.
     * @throws NotPermittedPropertyTypeError
     *             if at least one to the types in {@code properties} are not
     *             permitted.
     */
    public static String serialise(final Object[] properties)
            throws NullPointerException, NotPermittedPropertyTypeError {
        final StringBuilder result = new StringBuilder("[");

        doSerialisation(result, properties);

        return result.append("]").toString();
    }

    private static void doSerialisation(final StringBuilder toFill,
            final Object[] properties) throws NotPermittedPropertyTypeError {
        for (int current = 0; current < properties.length; current++) {
            final Object eachProperty = properties[current];

            // FIXME We could not support null, we do not know the type -> 
            // CHANGE THIS, remove null support
            if (eachProperty == null) {
                toFill.append("null");
                continue;
            }

            if (current > 0) {
                toFill.append(SEPARATOR);
            }

            // Normal arrays are not allowed, so we use UnmodifiableArray here:
            if (eachProperty instanceof UnmodifiableArray) {
                toFill.append('{');
                doSerialisation(toFill, ((UnmodifiableArray<?>) eachProperty)
                        .toArray());
                toFill.append('}');
                continue;
            }

            final Class<? extends Object> eachPropertysType = eachProperty
                    .getClass();

            if (Value.class.isAssignableFrom(eachPropertysType)) {
                final Value<?> value = Value.class.cast(eachProperty);
                toFill.append(value.getClass().getName());
                toFill.append(value.serialised());
                continue;
            }
            
            if( eachPropertysType.isEnum() )
            {
                // FIXME Add Contraints check here
                toFill.append(eachPropertysType.getName());
                toFill.append(".");
                toFill.append(((Enum<?>)eachProperty).name());
                continue;
            }

            final SerialisationStrategy strategy = STRATEGIES
                    .get(eachPropertysType);

            if (strategy == null) {
                throw new NotPermittedPropertyTypeError(eachPropertysType);
            }

            strategy.serialise(toFill, eachProperty);
        }
    }
}
